/*------------------------------------------------------------------------------
 * Copyright 2012 NCR Corporation
 *------------------------------------------------------------------------------
 * All revision information is updated automatically from source code control
 * change records - please do not manually edit.
 *------------------------------------------------------------------------------
 *
 *  $LastChangedRevision:: 1614                                     $
 *  $LastChangedBy:: rb250096                                       $
 *  $LastChangedDate:: 2012-02-24 01:54:40 -0500 (Fri, 24 Feb 2012) $
 */

package com.ncr.nep.dashboardcore.ui.client;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;

import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventListener;

/**
 * Utility class that wraps a DOM element and provides miscellaneous utility
 * functionality to it such as getting all children, append HTML, etc in a
 * similar fashion to jQuery.
 *
 * @author rb250096
 * @version $Revision: 1614 $
 */
public class DOMWrapper {

    /**
     * Specialized {@link EventListener} used to delegate a DOM event to
     * multiple listeners associated with a specified {@link Element}.
     */
    private class DelegatingEventListener implements EventListener {

        /**
         * List of user {@link EventListener} instances associated with the
         * element.
         */
        protected HashSet<EventListener> listeners =
                new HashSet<EventListener>();
        
        @Override
        public void onBrowserEvent(Event event) {
            for(EventListener l : listeners) {
                l.onBrowserEvent(event);
            }
        }
    }

    //--------------------------------------------------------------------------
    //
    //  Fields
    //
    //--------------------------------------------------------------------------

    /**
     * List of elements being wrapped for cases where manipulating multiple
     * nodes.
     */
    protected List<Element> elements;

    //--------------------------------------------------------------------------
    //
    //  Constructors
    //
    //--------------------------------------------------------------------------

    /**
     * Constructor
     *
     * @param elements The element to wrap.
     */
    public DOMWrapper(Element ...elements) {
        this.elements = Arrays.asList(elements);
    }

    /**
     * Constructor
     *
     * @param elements List of elements to wrap.
     */
    public DOMWrapper(List<Element> elements) {
        this.elements = elements;
    }

    /**
     * Gets the number of elements wrapped by this.
     *
     * @return The number of elements wrapped this.
     */
    public int count() {
        return this.elements.size();
    }
    
    //--------------------------------------------------------------------------
    //
    //  Methods
    //
    //--------------------------------------------------------------------------

    /**
     * Returns a DOMWrapper containing the list of {@link Element} objects.
     *
     * @param elements The list of elements to wrap.
     *
     * @return DOMWrapper containing the specified elements.
     */
    public static DOMWrapper $(Element ...elements) {
        return new DOMWrapper(elements);
    }

    /**
     * Returns a DOMWrapper containing the list of {@link Element} objects.
     *
     * @param elements The list of elements to wrap.
     *
     * @return DOMWrapper containing the specified elements.
     */
    public static DOMWrapper $(List<Element> elements) {
        return new DOMWrapper(elements);
    }
    
    /**
     * Removes all child nodes from the element.
     *
     * @return this
     */
    public DOMWrapper empty() {
        for(Element e : this.elements) {
            while(e.getChildCount() > 0) {
                e.removeChild(e.getChild(0));
            }
        }
        
        return this;
    }

    /**
     * Parses the specified HTML and appends it to the wrapped
     * node(s).
     *
     * @param html HTML String to parse and append.
     *
     * @return this
     */
    public DOMWrapper append(String html) {
        for(Element e : this.elements) {
            if(e.getChildCount() == 0) {
                e.setInnerHTML(html);
            }
            else {
                Element temp = DOM.createDiv();
                temp.setInnerHTML(html);
                NodeList<Node> newNodes = temp.getChildNodes();
                while(newNodes.getLength() > 0) {
                    e.appendChild(newNodes.getItem(0));
                }
            }
        }
        return this;
    }

    /**
     * Appends 0 or more {@link Element} instances to the wrapped nodes.  If
     * more than one node is wrapped by this, the specified elements being
     * appended will be cloned.
     *
     * @param newElements The elements being appended.
     *
     * @return this
     */
    public DOMWrapper append(Element ...newElements) {
        boolean clone = this.elements.size() > 1;
        for(Element e : this.elements) {
            for (Element n : newElements) {
                if(!clone) {
                    e.appendChild(n);
                }
                else {
                    e.appendChild(n.cloneNode(true));
                }
            }
        }

        return this;
    }

    /**
     * Gets the first element wrapped by this.
     *
     * @return DOMWrapper wrapping the first {@link Element} in this.
     */
    public DOMWrapper first() {
        if(this.elements.size() == 0) {
            return new DOMWrapper();
        }
        else {
            return new DOMWrapper(this.elements.get(0));
        }
    }

    /**
     * Gets the last element wrapped by this.
     *
     * @return DOMWrapper wrapping the last {@link Element} in this.
     */
    public DOMWrapper last() {
        if(this.elements.size() == 0) {
            return new DOMWrapper();
        }
        else {
            return new DOMWrapper(this.elements.get(this.elements.size() - 1));
        }
    }

    /**
     * Gets all children of the wrapped node.
     *
     * @return DOMWrapper wrapping the children of the current node.
     */
    public DOMWrapper children() {
        ArrayList<Element> children = new ArrayList<Element>();
        for(Element e : this.elements) {
            for(int i = 0, l = e.getChildCount(); i < l; i++) {
                Node n = e.getChild(i);
                if(n != null && n instanceof Element) {
                    children.add((Element)n);
                }
            }
        }

        return new DOMWrapper(children);
    }

    /**
     * Gets DOMWrapper wrapping the element at the specified index in this.
     * 
     * @param index The index of the element to retrieve from this.
     *              
     * @return DOMWrapper wrapping the element with the specified index.
     *
     * @throws IndexOutOfBoundsException Thrown if the index is out of bounds.
     * 
     */
    public DOMWrapper eq(int index) {
        if(index >= 0 && index <= this.elements.size()) {
            return new DOMWrapper(this.elements.get(index));
        }
        else {
            throw new IndexOutOfBoundsException();
        }
    }

    /**
     * Gets the underlying {@link Element} object at the specified index within
     * this.
     * 
     * @param index The index of the element.
     *              
     * @return The element object at the specified index.
     * 
     * @throws  IndexOutOfBoundsException Thrown if the index is out of bounds.
     */
    public Element get(int index) {
        return this.elements.get(index);
    }
    
    /**
     * Gets the value of the specified attribute for the first {@link Element}
     * in this.
     * 
     * @param name The name of the attribute.
     * 
     * @return The value of the attribute.
     */
    public String attr(String name) {
        if(this.elements.size() > 0) {
            Element e = this.elements.get(0);
            return e.getAttribute(name);
        }
        else {
            return null;
        }
    }

    /**
     * Sets the value of the specified attribute for the {@link Element}
     * instances in this.
     *
     * @param name The name of the attribute.
     * @param value The value to set for the attribute.
     *
     * @return this
     */
    public DOMWrapper attr(String name, String value) {
        for(Element e : this.elements) {
            e.setAttribute(name, value);
        }

        return this;
    }

    /**
     * Adds the specified CSS class name to each {@link Element} wrapped by
     * this.
     * 
     * @param className The name of the CSS class.
     *                  
     * @return this
     */
    public DOMWrapper addClass(String className) {
        for(Element e : this.elements) {
            e.addClassName(className);
        }
        
        return this;
    }

    /**
     * Removes the specified CSS class name from each {@link Element} wrapped
     * by this.
     *
     * @param className The name of the class to remove.
     *
     * @return this
     */
    public DOMWrapper removeClass(String className) {
        for(Element e : this.elements) {
            e.removeClassName(className);
        }

        return this;
    }

    /**
     * Binds the specified {@link EventListener} to the specified
     * DOM event for the wrapped {@link Element} instances in this.
     *
     * @param eventType One of the event constants defined in {@link Event}.
     * @param listener The event listener to invoke when the specified event
     *                 is received.
     *
     * @return this
     */
    public DOMWrapper bind(int eventType, EventListener listener) {
        for(Element e : this.elements) { 
            EventListener l = Event.getEventListener(e);
            if(l == null) {
                l = new DelegatingEventListener();
                Event.setEventListener(e, l);
            }
            else if(!(l instanceof DelegatingEventListener)) {
                throw new IllegalStateException("The specified element " +
                        "already has an EventListener object specified " +
                        "outside of DOMWrapper.");
            }
            
            DelegatingEventListener dl = (DelegatingEventListener)l;
            dl.listeners.add(listener);
            Event.sinkEvents(e, eventType);
        }

        return this;
    }

    /**
     * Unbinds the specified {@link EventListener} from the wrapped 
     * {@link Element} instances in this.
     *
     * @param eventType One of the event constants defined in {@link Event}.
     * @param listener The event listener to remove.
     *                 
     * @return this
     */
    public DOMWrapper unbind(int eventType, EventListener listener) {
        for(Element e : this.elements) {
            EventListener l = Event.getEventListener(e);
            if(l == null || !(l instanceof DelegatingEventListener)) {
                continue;
            }
            else {
                DelegatingEventListener dl = (DelegatingEventListener)l;
                dl.listeners.remove(listener);
            }
        }

        return this;
    }
}
